<?php

declare(strict_types=1);

namespace core;

use Exception;
use Throwable;

/**
 * 编译型模板引擎类
 */
class View
{
    /** @var string 模板文件根目录 (绝对路径) */
    protected string $view_path;

    /** @var string 缓存文件根目录 (绝对路径) */
    protected string $cache_path;

    /** @var bool 是否为调试模式 (生产环境应设为 false) */
    protected bool $debug_mode;

    /** @var array 全局分配的模板变量 */
    protected array $vars = [];

    /** @var array 编译期间的模板内容缓存 (用于处理 {extend}) */
    private array $loadedTemplateCache = [];

    /**
     * 构造函数
     * 
     * @param string $view_path  模板目录 (例如: /var/www/app/view)
     * @param string $cache_path 缓存目录 (例如: /var/www/app/runtime/view)
     * @throws Exception 如果目录不存在或不可写
     */
    public function __construct(
        string $view_path  = APP_PATH . '/view',
        string $cache_path = RUNTIME_PATH . '/view',
        bool $debug_mode   = APP_DEBUG
    ) {
        // 安全: 解析真实路径以防止路径混淆
        $this->view_path  = rtrim(realpath($view_path) ?: $view_path, DIRECTORY_SEPARATOR);
        $this->cache_path = rtrim(realpath($cache_path) ?: $cache_path, DIRECTORY_SEPARATOR);
        $this->debug_mode = $debug_mode;

        if (!is_dir($this->view_path)) {
            throw new Exception("模板目录不存在: {$this->view_path}");
        }

        if (!is_dir($this->cache_path) || !is_writable($this->cache_path)) {
            throw new Exception("缓存目录不存在或不可写: {$this->cache_path}");
        }
    }

    /**
     * 向模板分配全局变量
     *
     * @param string $key 变量名
     * @param mixed $value 变量值
     * @return void
     */
    public function assign(string $key, $value): void
    {
        $this->vars[$key] = $value;
    }

    /**
     * 渲染模板并返回内容
     *
     * @param string $template 模板名称 (如 'home/index')
     * @param array $data 临时传入的数据
     * @return string 渲染后的 HTML
     * @throws Exception
     */
    public function render(string $template, array $data = []): string
    {
        // 性能: 获取缓存文件路径
        $cache_file = $this->getCacheFilePath($template);

        // 性能优化: 生产模式 (非调试模式)
        if (!$this->debug_mode && file_exists($cache_file)) {
            // 缓存存在，直接执行，跳过所有 I/O 和路径检查
            return $this->executeView($cache_file, array_merge($this->vars, $data));
        }

        // 获取并验证模板文件路径 (I/O: realpath)
        $template_file = $this->getTemplateFilePath($template);

        // 检查缓存是否有效 (I/O: filemtime) 或是否需要编译
        // (在生产模式下，仅当缓存文件不存在时，!$this->isCacheValid 才会为 true)
        if ($this->debug_mode && $this->isCacheValid($template_file, $cache_file)) {
            // 调试模式下，缓存有效，跳过编译
        } else {
            // 生产模式下缓存不存在 或 调试模式下缓存失效，执行编译
            $this->compileTemplate($template, $cache_file);
        }

        // 合并全局变量和临时变量
        $vars = array_merge($this->vars, $data);

        // 执行已编译的 PHP 缓存文件
        return $this->executeView($cache_file, $vars);
    }

    /**
     * 显示模板内容 (直接输出)
     *
     * @param string $template 模板名称 (如 'home/index')
     * @param array $data 临时传入的数据
     * @return void
     */
    public function display(string $template, array $data = []): void
    {
        try {
            echo $this->render($template, $data);
        } catch (Throwable $e) {
            echo "模板渲染错误: " . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        }
    }

    /**
     * 执行视图文件并捕获输出
     *
     * @param string $cacheFile 缓存文件的绝对路径
     * @param array $vars 需要注入的变量
     * @return string 捕获的输出内容
     * @throws Exception 如果 include 失败
     */
    private function executeView(string $cacheFile, array $vars): string
    {
        // 将所有模板变量放入一个不太可能冲突的关联数组变量名 $__VIEW_DATA__ 中
        $__VIEW_DATA__ = $vars;

        ob_start();
        try {
            include $cacheFile;
        } catch (Throwable $e) {
            ob_end_clean(); // 渲染出错时丢弃已缓冲的内容
            throw new Exception("模板渲染执行失败: {$e->getMessage()}", (int)$e->getCode(), $e);
        }
        return ob_get_clean() ?: '';
    }

    /**
     * 编译模板文件并写入缓存
     *
     * @param string $template 模板名称 (如 'home/index')
     * @param string $cacheFile 缓存文件的绝对路径
     * @return void
     * @throws Exception 编译失败
     */
    protected function compileTemplate(string $template, string $cacheFile): void
    {
        // 性能: 编译开始时，清空 {extend} 继承链的缓存
        $this->loadedTemplateCache = [];

        try {
            /**
             * 安全警告:
             * 
             * 模板文件被视为“受信任”的代码
             * 此编译器 *不会* 移除或转义模板中已存在的 <?php ... ?> 标签
             * 这意味着任何可以编辑模板文件的人都具有完整的 RCE 权限
             * *绝不* 允许非开发人员或不受信任的用户编辑模板文件
             */

            // 直接使用模板名递归解析
            $content = $this->loadTemplateRecursive($template);

            // 编译变量和标签 (替换为原生PHP)
            $content = $this->compileTags($content);

            // 添加编译头部
            $header = "<?php /* 由 View 引擎编译于: " . date('Y-m-d H:i:s') . " */ ?>\n";
            $content = $header . $content;

            // 性能/安全: 使用原子操作写入缓存
            $tmpFile = tempnam($this->cache_path, 'view_');

            if ($tmpFile === false) {
                throw new Exception("创建临时缓存文件失败");
            }

            if (file_put_contents($tmpFile, $content) === false) {
                @unlink($tmpFile); // 清理
                throw new Exception("写入临时缓存文件失败");
            }

            if (rename($tmpFile, $cacheFile) === false) {
                @unlink($tmpFile); // 清理
                throw new Exception("移动缓存文件失败: $cacheFile");
            }

            @chmod($cacheFile, 0664); // 设置权限

        } catch (Throwable $e) {
            // 编译失败时清理，防止使用错误的缓存
            if (isset($tmpFile) && file_exists($tmpFile)) @unlink($tmpFile);
            // 编译失败时也删除目标缓存文件，防止（调试模式下）mtime 错误
            if (file_exists($cacheFile)) @unlink($cacheFile);
            throw new Exception("模板编译失败: {$e->getMessage()}", (int)$e->getCode(), $e);
        }
    }

    /**
     * 递归加载模板，处理继承
     *
     * @param string $templateName 模板名称 (如 'home/index')
     * @return string 解析后的模板内容
     * @throws Exception
     */
    protected function loadTemplateRecursive(string $templateName): string
    {
        // 性能: 检查 {extend} 编译缓存
        if (isset($this->loadedTemplateCache[$templateName])) {
            return $this->loadedTemplateCache[$templateName];
        }

        // 安全: 已通过 getTemplateFilePath 验证
        $file    = $this->getTemplateFilePath($templateName);
        $content = file_get_contents($file);
        if ($content === false) {
            throw new Exception("无法读取模板文件: $file");
        }

        // 处理 {extend} 继承
        if (preg_match('/{extend\s+name="([^"]+)".*?}/', $content, $matches)) {
            $parentTemplateName = $matches[1];
            // 递归编译父模板
            $parentContent = $this->loadTemplateRecursive($parentTemplateName);

            // 提取当前模板的区块
            $blocks = $this->extractBlocks($content);

            // 用当前区块内容替换父模板中的对应区块
            return $this->replaceBlocksInParent($parentContent, $blocks);
        }

        // 性能: 缓存结果
        $this->loadedTemplateCache[$templateName] = $content;
        return $content;
    }

    /**
     * 提取模板中的所有 {block}
     *
     * @param string $content 模板内容
     * @return array [区块名 => 区块内容]
     */
    protected function extractBlocks(string $content): array
    {
        $blocks = [];
        preg_match_all('/{block\s+name="([^"]+)"}(.*?)\{\/block\}/s', $content, $matches, PREG_SET_ORDER);
        foreach ($matches as $match) {
            $blocks[$match[1]] = $match[2]; // [区块名 => 内容]
        }
        return $blocks;
    }

    /**
     * 将子模板的区块内容替换到父模板中
     *
     * @param string $parentContent 父模板内容
     * @param array $blocks 子模板的区块 [区块名 => 区块内容]
     * @return string 替换后的内容
     */
    protected function replaceBlocksInParent(string $parentContent, array $blocks): string
    {
        // 用子模板的区块替换
        foreach ($blocks as $name => $content) {
            // 确保 preg_quote 的分隔符是 '/'
            $pattern = '/{block\s+name="' . preg_quote($name, '/') . '"}(.*?)\{\/block\}/s';
            // 限制替换 1 次
            $parentContent = preg_replace($pattern, $content, $parentContent, 1);
        }

        // 移除父模板中所有剩余的 (未被覆盖的) {block} 标签，保留其默认内容
        $parentContent = preg_replace('/{block\s+name="[^"]+"}(.*?)\{\/block\}/s', '$1', $parentContent);

        return $parentContent;
    }

    /**
     * 编译模板标签为原生 PHP
     *
     * @param string $content 模板内容
     * @return string 编译后的 PHP 内容
     */
    protected function compileTags(string $content): string
    {
        // --- 1. 编译 {if}, {elseif}, {else}, {/if} ---
        // {if $user.name == 'test'}
        $content = preg_replace_callback(
            '/\{if\s+(.+?)\}/',
            function ($matches) {
                // 使用 compileCondition 解析内部的变量
                return '<?php if (' . $this->compileCondition($matches[1]) . '): ?>';
            },
            $content
        );

        // {elseif $user.age > 18}
        $content = preg_replace_callback(
            '/\{elseif\s+(.+?)\}/',
            function ($matches) {
                return '<?php elseif (' . $this->compileCondition($matches[1]) . '): ?>';
            },
            $content
        );

        // {else}
        $content = preg_replace(
            '/\{else\}/',
            '<?php else: ?>',
            $content
        );

        // {/if}
        $content = preg_replace(
            '/\{\/if\}/',
            '<?php endif; ?>',
            $content
        );
        
        // --- 1.5 编译 {foreach} and {/foreach} ---

        // {foreach $list.path as $key => $item} 或 {foreach $list as $item}
        $content = preg_replace_callback(
            // 匹配 {foreach $list.path as $key => $item} 或 {foreach $list as $item}
            '/\{foreach\s+\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+as\s+\$([a-zA-Z_][a-zA-Z0-9_]*)(?:\s+=>\s+\$([a-zA-Z_][a-zA-Z0-9_]*))?\}/',
            function ($matches) {
                $listPath = $matches[1]; // 'list.path'
                $keyVar   = $matches[2]; // 'item' or 'key'
                $itemVar  = isset($matches[3]) ? $matches[3] : $matches[2]; // 'item' (if key is present) or 'item' (if key is missing)

                // 列表变量的 PHP 访问
                $listAccess = $this->buildPhpVariableAccess($listPath);
                
                // 处理 as $key => $item 格式
                if (isset($matches[3])) {
                    // 格式: {foreach $list as $key => $item}
                    $asClause = "\${$keyVar} => \${$itemVar}";
                } else {
                    // 格式: {foreach $list as $item} (此时 $keyVar 实际上是 $item 的名字)
                    $asClause = "\${$keyVar}"; 
                }

                // $listAccess ?? [] : 如果列表变量未定义，则使用空数组
                return '<?php foreach (' . $listAccess . ' ?? [] as ' . $asClause . '): ?>';
            },
            $content
        );

        // {/foreach}
        $content = preg_replace(
            '/\{\/foreach\}/',
            '<?php endforeach; ?>',
            $content
        );

        // --- 2. 编译变量 {$...} ---
        // 改进: 支持点语法 {$user.name} 和 {$var}
        $content = preg_replace_callback(
            // 匹配 {$var.name.profile} 或 {$var}，以及可选的 |raw
            '/\{\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(\|raw)?\}/',
            function ($matches) {
                $varPath = $matches[1]; // 获取变量路径，例如 'user.name' 或 'user'
                $isRaw   = isset($matches[2]) && $matches[2] === '|raw'; // 检查是否有 |raw 过滤器

                // 使用 helper 构建 PHP 访问表达式
                $phpAccess = $this->buildPhpVariableAccess($varPath);
                
                // 添加 ?? '' (null 合并) 以防止未定义键时出错
                $phpAccessWithNull = "($phpAccess ?? '')";

                if ($isRaw) {
                    // |raw: 直接输出
                    return '<?php echo ' . $phpAccessWithNull . '; ?>';
                } else {
                    // 默认: 自动转义
                    return '<?php echo htmlspecialchars(' . $phpAccessWithNull . ', ENT_QUOTES | ENT_SUBSTITUTE, \'UTF-8\'); ?>';
                }
            },
            $content
        );

        return $content;
    }

    /**
     * 编译 {if} / {elseif} 标签中的条件表达式
     *
     * @param string $condition 模板中的原始条件 (例如: "$user.name == 'test' AND $user.age > 18")
     * @return string 编译为 PHP 的条件
     */
    protected function compileCondition(string $condition): string
    {
        // 匹配所有 $var.path.style 变量
        $condition = preg_replace_callback(
            // 匹配 $var.name.profile 或 $var
            '/\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/',
            function ($matches) {
                $varPath = $matches[1]; // 'user.name' or 'user'

                $phpAccess = $this->buildPhpVariableAccess($varPath);
                
                // 在 {if} 条件中，我们使用 ( ... ?? null) 
                // 解释：防止在条件中使用未定义的变量导致 PHP 报错。
                return "($phpAccess ?? null)";
            },
            $condition
        );

        return $condition;
    }

    /**
     * 将点语法变量路径 (如 'user.name') 转换为 PHP 数组访问表达式
     *
     * @param string $varPath 模板变量路径，不带 $ 符号 (e.g., 'user.name')
     * @return string PHP 表达式 (e.g., '$__VIEW_DATA__[\'user\'][\'name\']')
     */
    private function buildPhpVariableAccess(string $varPath): string
    {
        // 将点语法 'user.name' 转换为 PHP 数组访问
        $parts = explode('.', $varPath);

        // $__VIEW_DATA__['user']['name']
        $phpAccess = '$__VIEW_DATA__';
        foreach ($parts as $part) {
            // 使用 var_export 确保键名被安全地转义为 PHP 字符串字面量
            $phpAccess .= '[' . var_export($part, true) . ']';
        }
        return $phpAccess;
    }

    /**
     * 获取并验证模板文件路径
     *
     * @param string $template 模板名称 (如 'home/index')
     * @return string 模板文件的绝对路径
     * @throws Exception 如果文件非法或不存在
     */
    protected function getTemplateFilePath(string $template): string
    {
        // 移除 '..' 和空字节，防止路径遍历
        $template = str_replace(["..", "\0"], '', $template);
        $file     = $this->view_path . DIRECTORY_SEPARATOR . ltrim($template, '/\\') . '.php';

        // 再次使用 realpath 确认文件在 $view_path 目录内
        $realFile = realpath($file);
        if ($realFile === false) {
            throw new Exception("模板文件不存在: $file");
        }

        // 安全: 确保解析的真实路径仍然在 $view_path 之下
        if (strpos($realFile, $this->view_path) !== 0) {
            throw new Exception("模板文件路径非法: $file");
        }

        return $realFile;
    }

    /**
     * 获取缓存文件路径
     *
     * @param string $template 模板名称 (如 'home/index')
     * @return string 缓存文件的绝对路径
     */
    protected function getCacheFilePath(string $template): string
    {
        // 性能: 使用 md5 确保文件名合法且唯一
        // 增加 $this->view_path 作为盐，确保不同模板目录的同名文件缓存不同
        $hash = md5($this->view_path . '::' . $template);
        return $this->cache_path . DIRECTORY_SEPARATOR . $hash . '.php';
    }

    /**
     * 检查缓存是否有效 (仅在 debugMode = true 时有意义)
     *
     * @param string $templateFile 模板文件的绝对路径
     * @param string $cacheFile 缓存文件的绝对路径
     * @return bool 缓存是否有效
     */
    protected function isCacheValid(string $templateFile, string $cacheFile): bool
    {
        if (!file_exists($cacheFile)) {
            return false;
        }

        // 生产模式下 (debugMode=false)，render() 方法会跳过此检查。
        // 此检查仅在调试模式下运行。
        // 检查模板文件（或它继承的任何文件，虽然这里没检查）的修改时间
        // TODO: 为了完美，{extend} 的 mtime 也应该被检查，但这很复杂。
        // 对于小项目，仅检查当前文件 mtime 是一个合理的权衡。
        return filemtime($templateFile) <= filemtime($cacheFile);
    }
}
